/*
 * FeedSyncronizer.java
 *
 * Created on April 11, 2007, 8:13 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app.sync;

import java.io.IOException;
import java.net.URI;
import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.AtomResource;
import org.atomojo.app.Storage;
import org.atomojo.app.auth.AuthCredentials;
import org.atomojo.app.client.FeedClient;
import org.atomojo.app.client.StatusException;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.EntryMedia;
import org.atomojo.app.db.Feed;
import org.atomojo.app.db.Journal;
import org.atomojo.app.db.SyncProcess;
import org.infoset.xml.XMLException;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Status;
import org.restlet.representation.Representation;

/**
 *
 * @author alex
 */
public class PushSynchronizer implements Synchronizer
{
   DB db;
   Storage storage;
   Logger log;
   String username;
   String password;
   SyncProcess proc;
   Date syncTime;
   int errorCount;
   
   
   /** Creates a new instance of FeedSyncronizer */
   public PushSynchronizer(Logger log,DB db,Storage storage,SyncProcess proc)
   {
      this.db = db;
      this.storage = storage;
      this.log = log;
      this.username = "admin";
      this.password = "admin";
      this.proc = proc;
      this.syncTime = null;
      if (proc.isPullSynchronization()) {
         throw new IllegalArgumentException("The sync process "+proc.getName()+" is not push synchronization.");
      }
      this.errorCount = 0;
   }
   
   public int getErrorCount() {
      return errorCount;
   }
   
   public SyncProcess getProcess() {
      return proc;
   }
   
   public Date getSynchronizedAt() {
      return syncTime;
   }
   
   public void setSynchronizationAt(Date time)
   {
      this.syncTime = time;
   }
   
   public void sync() 
      throws SyncException
   {
      errorCount = 0;
      if (syncTime==null) {
         syncTime = new Date();
      }
      if (proc.getRemoteApp()==null) {
         throw new SyncException("The remote application cannot be found.");
      }
      if (proc.getRemoteApp().isAvailable()) {
         try {
            String startPath = proc.getSyncTarget().getPath();
            if (startPath.length()>0) {
               errorCount++;
               throw new SyncException("Paths are not current supported for push synchronization.");
            }
            Date lastSync = proc.getLastSynchronizedOn();
            log.info("Synchronizing "+proc.getRemoteApp().getRoot()+", "+(lastSync==null ? "first sync." : "last sync was at "+AtomResource.toXSDDate(lastSync)));
            Iterator<Journal.Entry> entries = db.getJournal().getDeletesSince(lastSync,syncTime);
            AuthCredentials auth = proc.getRemoteApp().getAuthCredentials();
            while (entries.hasNext()) {

               Journal.Entry jentry = entries.next();

               if (jentry.getOperation()==Journal.DELETE_OPERATION) {
                  Journal.DeleteEntry deleteEntry = (Journal.DeleteEntry)jentry;
                  UUID feed = deleteEntry.getFeed();
                  UUID entry = deleteEntry.getEntry();
                  URI feedURI = proc.getRemoteApp().getRoot().resolve(deleteEntry.getPath());
                  if (entry==null) {
                     // delete feed
                     log.info("Deleting feed "+feedURI);
                     FeedClient appClient = new FeedClient(feedURI);
                     if (auth!=null) {
                        appClient.setIdentity(auth.getName(),auth.getPassword());
                     }
                     Status status = appClient.delete();
                     if (!status.isSuccess() && !status.equals(Status.CLIENT_ERROR_NOT_FOUND)) {
                        errorCount++;
                        log.severe("Cannot delete feed "+feedURI+", stopping synchronization.");
                        throw new SyncException("Synchronization was incomplete.  Stopped due to failure to delete feed: "+feedURI);
                     }
                  } else {
                     // delete entry
                     log.info("Deleting entry "+entry+" in feed "+feedURI);
                     FeedClient appClient = new FeedClient(feedURI);
                     if (auth!=null) {
                        appClient.setIdentity(auth.getName(),auth.getPassword());
                     }
                     Status status = appClient.deleteEntry(entry);
                     if (!status.isSuccess() && !status.equals(Status.CLIENT_ERROR_NOT_FOUND)) {
                        errorCount++;
                        log.severe("Cannot delete feed "+feedURI+", stopping synchronization.");
                        throw new SyncException("Synchronization was incomplete.  Stopped due to failure to delete feed: "+feedURI);
                     }
                  }
               }
            }
            entries = db.getJournal().getUpdatesSince(lastSync,syncTime);
            while (entries.hasNext()) {

               Journal.UpdatedEntry updateEntry = (Journal.UpdatedEntry)entries.next();

               Feed feed = updateEntry.getFeed();
               Entry entry = updateEntry.getEntry();
               String path = feed.getPath();
               if (path.length()>0 && !path.endsWith("/")) {
                  path += "/";
               }
               URI feedURI = proc.getRemoteApp().getRoot().resolve(path);
               FeedClient appClient = new FeedClient(feedURI);
               if (auth!=null) {
                  appClient.setIdentity(auth.getName(),auth.getPassword());
               }


               if (updateEntry.getOperation()==Journal.CREATE_OPERATION) {
                  // Create feed or entry
                  if (entry==null) {
                     // Create feed
                     log.info("Creating feed "+feedURI);
                     try {
                        Representation feedRep = storage.getFeed(path,feed.getUUID(),feed.getEntries());
                        // Now, create the feed with the XML
                        try {
                           String xml = feedRep.getText();
                           if (!appClient.create(xml).isSuccess()) {
                              errorCount++;
                              log.severe("Cannot create feed on target for path "+path);
                              throw new SyncException("Synchronization was incomplete.  Stopped due to failure to create feed: "+path);
                           }
                        } catch (IOException ex) {
                           errorCount++;
                           log.log(Level.SEVERE,"Cannot get xml serialization of feed document for path "+path,ex);
                           throw new SyncException("Synchronization was incomplete.  Stopped due to failure of serialization at path "+path);
                        }
                     } catch (IOException ex) {
                        errorCount++;
                        log.log(Level.SEVERE,"Cannot get feed document for path "+path,ex);
                        throw new SyncException("Synchronization was incomplete.  Stopped due failure to get feed at "+path);
                     }
                  } else {
                     // Create entry

                     log.info("Creating entry "+entry.getUUID()+" for feed "+feedURI);

                     // Check for media entries
                     Iterator<EntryMedia> media = entry.getResources();
                     if (media.hasNext()) {

                        // We have a media entry and so we post the media first
                        EntryMedia resource = media.next();
                        if (media.hasNext()) {
                           while (media.hasNext()) {
                              media.next();
                           }
                           errorCount++;
                           log.severe("Media entries with more than one resource is not supported.");
                           throw new SyncException("Synchronization was incomplete.  Media entry for entry "+entry.getUUID()+" has more than one resource.");
                        }

                        // Get the media from the XML DB
                        try {
                           Representation rep = storage.getMedia(path,feed.getUUID(),resource.getName());
                           // Send the media to the server
                           rep.setMediaType(resource.getMediaType());
                           appClient.createMedia(entry.getUUID(),resource.getName(),rep);
                        } catch (StatusException ex) {
                           errorCount++;
                           log.severe("Cannot update media "+resource.getName()+" on target for path "+path+", status="+ex.getStatus().getCode());
                           throw new SyncException("Synchronization was incomplete.  Failure to update media "+resource.getName()+" for entry "+entry.getUUID()+", status="+ex.getStatus().getCode());
                        } catch (Exception ex) {
                           errorCount++;
                           log.log(Level.SEVERE,"Cannot get media "+resource.getName()+" for path "+path+" and id "+entry.getUUID().toString(),ex);
                           throw new SyncException("Synchronization was incomplete.  Failure to get media "+resource.getName()+" for entry "+entry.getUUID());
                        }
                     } else {

                        String xml = null;
                        // Get the entry document from the XML DB
                        try {
                           Representation entryRep = storage.getEntry(".",path,feed.getUUID(),entry.getUUID());

                           // Now, create the feed with the XML
                           try {
                              xml = entryRep.getText();
                           } catch (IOException ex) {
                              errorCount++;
                              log.log(Level.SEVERE,"Cannot get xml serialization of entry document for path "+path,ex);
                              throw new SyncException("Synchronization was incomplete.  Failure to get serialization of entry "+entry.getUUID());
                           }
                        } catch (IOException ex) {
                           errorCount++;
                           log.log(Level.SEVERE,"Cannot get entry document for path "+path+" and id "+entry.getUUID().toString(),ex);
                           throw new SyncException("Synchronization was incomplete.  Failure to get entry "+entry.getUUID());
                        }

                        // This is a regular entry
                        try {
                           appClient.createEntry(xml);
                        } catch (Exception ex) {
                           errorCount++;
                           log.severe("Cannot create entry on target for path "+path);
                           throw new SyncException("Synchronization was incomplete.  Failure to create entry "+entry.getUUID()+" for path "+path);
                        }
                     }
                  }
               } else {
                  EntryMedia resource = updateEntry.getResource();
                  if (entry==null) {
                     // Modify feed
                     log.info("Updating feed "+feedURI);
                     // Get the feed document from the XML DB
                     try {
                        Representation feedRep = storage.getFeed(path,feed.getUUID(),feed.getEntries());

                        // Now, create the feed with the XML
                        try {
                           String xml = feedRep.getText();
                           if (!appClient.update(xml).isSuccess()) {
                              errorCount++;
                              log.severe("Cannot update feed on target for path "+path);
                              throw new SyncException("Synchronization was incomplete.  Cannot update feed for path "+path);
                           }
                        } catch (IOException ex) {
                           errorCount++;
                           log.log(Level.SEVERE,"Cannot get xml serialization of feed document for path "+path,ex);
                           throw new SyncException("Synchronization was incomplete.  Cannot get serialization of feed for path "+path);
                        }
                     } catch (IOException ex) {
                        errorCount++;
                        log.log(Level.SEVERE,"Cannot get feed document for path "+path,ex);
                        throw new SyncException("Synchronization was incomplete.  Cannot get feed for path "+path);
                     }
                  } else if (resource==null) {
                     // Modify entry
                     log.info("Updating entry "+entry.getUUID()+" for feed "+feedURI);

                     // Get the entry document from the XML DB
                     try {
                        Representation entryRep = storage.getEntry(".",path,feed.getUUID(),entry.getUUID());

                        // Now, create the feed with the XML
                        try {
                           String xml = entryRep.getText();
                           if (!appClient.updateEntry(entry.getUUID(),xml).isSuccess()) {
                              errorCount++;
                              log.severe("Cannot update entry on target for path "+path);
                              throw new SyncException("Synchronization was incomplete.  Cannot update entry "+entry.getUUID()+" for path"+path);
                           }
                        } catch (IOException ex) {
                           errorCount++;
                           log.log(Level.SEVERE,"Cannot get xml serialization of entry document for path "+path,ex);
                           throw new SyncException("Synchronization was incomplete.  Cannot serialization of entry "+entry.getUUID()+" for path"+path);
                        }
                     } catch (IOException ex) {
                        errorCount++;
                        log.log(Level.SEVERE,"Cannot get entry document for path "+path+" and id "+entry.getUUID().toString(),ex);
                        throw new SyncException("Synchronization was incomplete.  Cannot get entry "+entry.getUUID()+" for path"+path);
                     }
                  } else {
                     // Modify resource
                     log.info("Updating resource "+resource.getName()+" for entry "+entry.getUUID()+" for feed "+feedURI);

                     // Get the media from the XML DB
                     try {
                        Representation mediaRep = storage.getMedia(path,feed.getUUID(),resource.getName());
                        mediaRep.setMediaType(resource.getMediaType());
                        if (!appClient.updateMedia(resource.getName(),mediaRep).isSuccess()) {
                           errorCount++;
                           log.severe("Cannot update media "+resource.getName()+" on target for path "+path);
                           throw new SyncException("Synchronization was incomplete.  Cannot update media "+resource.getName()+" for entry "+entry.getUUID()+" for path"+path);
                        }
                     } catch (IOException ex) {
                        errorCount++;
                        log.log(Level.SEVERE,"Cannot get media "+resource.getName()+" for path "+path+" and id "+entry.getUUID().toString(),ex);
                        throw new SyncException("Synchronization was incomplete.  Cannot get media "+resource.getName()+" for entry "+entry.getUUID()+" for path"+path);
                     }
                  }
               }
            }
            proc.setLastSynchronizedOn(syncTime);
            proc.marshall();
            proc.update();
         } catch (SQLException ex) {
            errorCount++;
            log.log(Level.SEVERE,"Cannot traverse journal due to SQL exception: "+ex.getMessage(),ex);
         } catch (XMLException ex) {
            errorCount++;
            log.log(Level.SEVERE,"Cannot iterate sync targets due to XML exception: "+ex.getMessage(),ex);
         }
      } else {
         errorCount++;
         log.warning("Target "+proc.getName()+" -> "+proc.getRemoteApp().getRoot()+" is not available.");
         throw new TargetNotAvailableException("Target "+proc.getName()+" -> "+proc.getRemoteApp().getRoot()+" is not available.");
      }
   }
   
   protected boolean feedExists(URI location)
   {
      Client client = new Client(new Context(log),Protocol.valueOf(location.getScheme()));
      client.getContext                                                                                                                                                             ().getAttributes().put("hostnameVerifier", org.apache.commons.ssl.HostnameVerifier.DEFAULT);
      Request request = new Request(Method.HEAD,location.toString());
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,username,password));
      Response response = client.handle(request);
      return response.getStatus().isSuccess();

   }
}
